#!/usr/bin/python
#
# Filename: mergeCal.py
#
# Description:
#       Collect calendars from GCal and merge calendars for sharing.
# 
# Author: Dinh Bowman
#
#=============================================================================

try:
    from xml.etree import ElementTree
except ImportError:
    from elementtree import ElementTree
import gdata.calendar.service
import gdata.calendar
import gdata.service
import atom.service
import atom
import sys
import string
import time
import re
import base64
import getpass
from optparse import OptionParser
import ConfigParser, os


class MergeCal:
    
    # Because Google only allows certain colors, we have to use a lookup
    #  Note: Index starts at 1.
    colorLookup = ('',
                    '#A32929',
                    '#B1365F',  
                    '#7A367A',
                    '#5229A3',
                    '#29527A',
                    '#2952A3',
                    '#1B887A',
                    '#28754E',
                    '#0D7813',
                    '#528800',
                    '#88880E',
                    '#AB8B00',
                    '#BE6D00',
                    '#B1440E',
                    '#865A5A',
                    '#705770',
                    '#4E5D6C',
                    '#5A6986',
                    '#4A716C',
                    '#6E6E41',
                    '#8D6F47')
	

    def __init__(self, email, password):
        """Creates an authenticated client connection with Google."""
        self.cal_client = gdata.calendar.service.CalendarService()
        self.cal_client.email = email
        self.cal_client.password = password
        self.cal_client.source = 'VI-MergeCalendar-0.1'
        self.cal_client.ProgrammaticLogin()


    def InitCalendar(self, calEntry, aclUsers, aclAccess):
        """Clears the calendar if it exists, creates it if it doesn't."""
        feed = self.cal_client.GetOwnCalendarsFeed()
        # Delete the calendar if it exists
        calFound = False
        print "Searching for existing calendar \"%s\"" % calEntry.title.text
        for eachCalendar in feed.entry:
            if eachCalendar.title.text == calEntry.title.text:
                print "Found it!"
                # It is possible to delete the calendar instead of clearing:
                #self.cal_client.Delete(eachCalendar.GetEditLink().href)
                # Its better to clear the calendar so that all ACL settings
                # are not lost and the notification email for shared access
                # isn't sent out each time we update the calendar.
                calID = self.GetID(eachCalendar.title.text)
                query = gdata.calendar.service.CalendarEventQuery(calID, 'private', 'full',
                                                                  params={'max-results':'999999'})
                feed = self.cal_client.CalendarQuery(query)
                for i, f in enumerate(feed.entry):
                    print "Clearing events %i of %i" % (i+1, len(feed.entry))
                    self.cal_client.DeleteEvent(f.GetEditLink().href)
                print "Done clearing"
                newCalendar = eachCalendar
                calFound = True
                break
        
        if calFound == False:
            # Create it
            print "Creating Calendar %s" % calEntry.title.text
            newCalendar = self.cal_client.InsertCalendar(new_calendar=calEntry)
        
        # Set the Access Control List (ACL)
        aclURI = "/calendar/feeds/%s/acl/full" % self.GetID(newCalendar.id.text, True)
        aclFeed = self.cal_client.GetCalendarAclFeed(uri=aclURI)
        aclUrl = newCalendar.GetAclLink().href 
        for i in range(0, len(aclUsers)):
            rule = gdata.calendar.CalendarAclEntry()
            roleValue = 'http://schemas.google.com/gCal/2005#%s' % (aclAccess[i])
            rule.role = gdata.calendar.Role(value=roleValue)
            rule.scope = gdata.calendar.Scope(value=aclUsers[i], scope_type='user')
            usrFound = False
            # Check if this user is already in the ACL
            for acl in aclFeed.entry:
                if aclUsers[i] == acl.scope.value:
                    # User is already in ACL... Update instead
                    print "Updating ACL for %s as %s" % (aclUsers[i], aclAccess[i])
                    self.cal_client.UpdateAclEntry(acl.GetEditLink().href, rule)
                    usrFound = True
                    break
            if not usrFound:
                print "Creating ACL for %s as %s" % (aclUsers[i], aclAccess[i])
                self.cal_client.InsertAclEntry(rule, aclUrl)
                

        return newCalendar


    def NewCalendarEntry(self, title="Merged Calendar",
         description="This calendar contains the results of a merge current and upcoming events.",
         time_zone="America/Los_Angeles", color=14, location="Seattle", hidden=False):
        """Creates a new Google calendar entry element."""
        newCalendar = gdata.calendar.CalendarListEntry()
        newCalendar.title = atom.Title(text=title)
        newCalendar.summary = atom.Summary(text=description)
        newCalendar.where = gdata.calendar.Where(value_string=location)
        newCalendar.color = gdata.calendar.Color(value=self.colorLookup[color])
        newCalendar.timezone = gdata.calendar.Timezone(value=time_zone)
        if hidden:
          newCalendar.hidden = gdata.calendar.Hidden(value='true')
        else:
          newCalendar.hidden = gdata.calendar.Hidden(value='false')
        
        return newCalendar


    def MergeEvents(self, calFromTitle, calFrom, calTo):
        """Copy events from one calendar and appends it to another."""
        query = gdata.calendar.service.CalendarEventQuery(calFrom, 'private', 'full',
                                                          params={'max-results':'999999'})
        feed = self.cal_client.CalendarQuery(query)
        for event in feed.entry:
            calToURI = "/calendar/feeds/%s/private/full" % calTo
            print "%s: Copying %s........................." % (calFromTitle, event.title.text) ,
            try:
                self.cal_client.InsertEvent(event, calToURI)
            except gdata.service.RequestError:
                print "Error! - Unable to add event."
            else:
                print "Done!"


    def GetID(self, calName, idText=False):
        """Return the ID of the calendar with the title given as the parameter"""

        # Create the regular expression
        filterID = re.compile(r"""
         (?:/full/)           # The beginning boundary of the id
         (?P<id>.*$)          # The calendar id is located at the end
        """, re.VERBOSE)

        calID = ""
        if idText == False:
            # Iterate through the calendars and find the calendar we're referencing
            feed = self.cal_client.GetOwnCalendarsFeed()
            for eachCalendar in feed.entry:
                if eachCalendar.title.text == calName:
                    calID = filterID.search(eachCalendar.id.text).group('id')
                    break
        else:
            # calName contains the calendar.id.text attribute instead of the name string
            calID = filterID.search(calName).group('id')

        return calID


    def GetName(self, calID):
        """Return the name of the calendar with the id given as the parameter"""
        return self.FetchCal(calID).title.text


    def FetchCal(self, calID):
        """Return the calendar entry element when given an id"""
        calURI = "/calendar/feeds/%s/private/full" % calID
        return self.cal_client.GetOwnCalendarsFeed(uri=calURI)
        

# END MergeCal class


def main():
    """Main"""

    # Handle any command line arguments
    parser = OptionParser()
    parser.add_option("-c", "--conf", dest="filename", action="store", type="string",
                      help="configuration filename", metavar="FILE", default="mergeCal.conf")
    parser.add_option("-p", "--setpasswd", dest="setPasswd", action="store_true",
                      help="set the password for login", default=False)
    (options, args) = parser.parse_args()

    # Open the configuration file
    mConf = ConfigParser.ConfigParser()
    configFile = open(options.filename, "r")
    mConf.readfp(configFile)
    configFile.close()
    
    # Set up our CSV parser
    csvParse = re.compile(r'\W*,\W*')   # Kill spaces on both sides of a comma
    
    # If the user wants to set the password
    if options.setPasswd:
        rawPasswd = getpass.getpass()
        newPasswd = base64.b64encode(rawPasswd)
        configFile = open(options.filename, "w")
        mConf.set("Google", "passwd", newPasswd)
        mConf.write(configFile)
        print "New password saved to configuration %s" % options.filename
        configFile.close()
        return

    # Instantiate the merging class
    mCal = MergeCal(mConf.get("Google", "username"),
                    base64.b64decode(mConf.get("Google", "passwd")))
    
    # Calendar to put merged results into
    calEntry = mCal.NewCalendarEntry(title=mConf.get("Destination", "name"),
                               description=mConf.get("Destination", "description"),
                                     color=mConf.getint("Destination", "color"),
                                  location=mConf.get("Destination", "location"),
                                 time_zone=mConf.get("Destination", "time_zone"),
                                    hidden=mConf.getboolean("Destination", "hidden"))
    
    # Setup our access user lists
    aclUsers = csvParse.split(mConf.get("Shared With", "users"))
    aclAccess = csvParse.split(mConf.get("Shared With", "access"))
    if len(aclUsers) != len(aclAccess):
        print "There is not an equal number of users and access rights in the config file!"
        return
    
    # Setup our new calendar
    mCal.InitCalendar(calEntry, aclUsers, aclAccess)
    mergeID = mCal.GetID(mConf.get("Destination", "name"))
    
    # Read data from all the source calendars, and for each one, add it to our destination
    for cal in csvParse.split(mConf.get("Source", "sources")):
        mCal.MergeEvents(mCal.GetName(cal), cal, mergeID)

    # Done
    print "Done"

if __name__ == '__main__':
    main()
